/* Gluten Library -- Texture
 *
 * A "classy" interface to an OpenGL texture resource.
 *
 * $AUTHOR$    res
 * $UPDATE$    r40
 */

/* LICENSES ********************************************************************

Copyright (c) 2013- Reuben E. Smith

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*******************************************************************************/

#ifdef DEBUG
   #include <cassert>
   #include <iostream>
#endif

#include <fstream>
#include <string>
#include "Texture.hpp"
#include <GL/freeglut.h>


namespace Gluten
{
   // res: There other other, OS/2-specific values that might pop up from
   //      some graphics editors. Do we care?
   // ASCII 'BM', big endian
   const unsigned short BITMAP_MAGIC_ID = 0x4D42;


   // Packing is necessary as the first two fields do not align properly. This
   // Should(TM) be cross-platform enough as both MSVC and GCC both support the
   // pack pragma.
   #pragma pack(2)
   struct BitmapFileHeader
   {
      uint16_t magic;
      uint32_t size;
      uint16_t reserved1;
      uint16_t reserved2;
      uint32_t offset;
   };
   #pragma pack()


   struct BitmapInfoHeader
   {
      uint32_t size;
      int32_t  width;
      int32_t  height;
      uint16_t planes;
      uint16_t bitsPerPixel;
      uint32_t compressionType;
      uint32_t imageSize;
      int32_t  horzPixelsPerMeter;
      int32_t  vertPixelsPerMeter;
      uint32_t colorsUsed;
      uint32_t colorsImportant;
   };


   enum BitmapCompressionType
   {
      BCT_RGB = 0,
      BCT_BITFIELDS = 3,
   };


   struct BitmapPaletteItem
   {
      uint8_t b;
      uint8_t g;
      uint8_t r;
      uint8_t a;
   };


   Image BitmapImageLoader::operator()(const std::string& path)
   {
      Image r;

      if (path.empty()) {
         return r;
      }

      std::ifstream file(path.c_str(), std::ios::in | std::ios::binary);

      if (!file.is_open()) {
         return r;
      }

      BitmapFileHeader fileHeader;
      BitmapInfoHeader infoHeader;

      file.read(reinterpret_cast<char*>(&fileHeader), 
         sizeof(BitmapFileHeader));

      // Magic number as defined by bitmap specification. Equivalent to 'BM'
      // ASCII values, big-endian.
      if (fileHeader.magic != BITMAP_MAGIC_ID) {
         file.close();
         return r;
      }

      file.read(reinterpret_cast<char*>(&infoHeader),
         sizeof(BitmapInfoHeader));

      if (infoHeader.compressionType != BCT_RGB &&
          infoHeader.compressionType != BCT_BITFIELDS) {
         file.close();
         return r;
      }

      // Reading relies on valid width and height data.
      if (infoHeader.width == 0 || infoHeader.height == 0) {
         file.close();
         return r;
      }

      bool isReversed = infoHeader.height < 0;
      // Width never should be negative, right...?
      infoHeader.width = std::abs(infoHeader.width);
      infoHeader.height = std::abs(infoHeader.height);

      size_t rowSize = (infoHeader.bitsPerPixel >> 3) * infoHeader.width;
      size_t padSize = (rowSize % 4) ? 4 - (rowSize % 4) : 0;

      if (infoHeader.imageSize == 0) {
         infoHeader.imageSize = (rowSize + padSize) * infoHeader.height;
      }

      r.channels = 3;
      r.layers = 1;
      r.width = infoHeader.width;
      r.height = infoHeader.height;

      BitmapPaletteItem* palette = nullptr;
      uint32_t maskR = 0, maskG = 0, maskB = 0, maskA = 0;

      if (infoHeader.bitsPerPixel <= 8) {
         palette = new BitmapPaletteItem[1 << infoHeader.bitsPerPixel];

         file.seekg(sizeof(BitmapFileHeader) + infoHeader.size);
         file.read(reinterpret_cast<char*>(palette),
            (1 << infoHeader.bitsPerPixel) * sizeof(BitmapPaletteItem));
      }
      else if (infoHeader.bitsPerPixel == 32) {
         // res: Not 100% on the order of these in the file. Assuming BGRA
         //      like everything else in bitmaps.
         file.read(reinterpret_cast<char*>(&maskB), sizeof(uint32_t));
         file.read(reinterpret_cast<char*>(&maskG), sizeof(uint32_t));
         file.read(reinterpret_cast<char*>(&maskR), sizeof(uint32_t));
         file.read(reinterpret_cast<char*>(&maskA), sizeof(uint32_t));

         r.channels = 4;
      }

      file.seekg(fileHeader.offset);

      // res: I feel like something in-place could be done to lower memory
      //      requirements a little.
      r.data = new float[r.channels * r.width * r.height];
      unsigned char* rawData = new unsigned char[infoHeader.imageSize];
      file.read(reinterpret_cast<char*>(rawData), infoHeader.imageSize);

      size_t x = 0, y = 0;
      if (isReversed) {
         y = r.height - 1;
      }

      for (size_t i = 0; i < infoHeader.imageSize;) {
         size_t px = r.channels * (x + y * r.width);

         if (infoHeader.bitsPerPixel <= 8) {
            size_t index = 0;

            if (infoHeader.bitsPerPixel == 1) {
               for (unsigned int j = 1; j <= 8; ++j) {
                  index = (rawData[i] & (1 << (8 - j))) >> (8 - j);
                  r.data[px + 0] = palette[index].r / 255.0f;
                  r.data[px + 1] = palette[index].g / 255.0f;
                  r.data[px + 2] = palette[index].b / 255.0f;

                  px += r.channels;
               }
               
               x += 8;
               ++i;
            }
            else if (infoHeader.bitsPerPixel == 4) {
               index = (rawData[i] & 0xF0) >> 4;
               r.data[px + 0] = palette[index].r / 255.0f;
               r.data[px + 1] = palette[index].g / 255.0f;
               r.data[px + 2] = palette[index].b / 255.0f;

               px += r.channels;
               index = rawData[i] & 0x0F;
               r.data[px + 0] = palette[index].r / 255.0f;
               r.data[px + 1] = palette[index].g / 255.0f;
               r.data[px + 2] = palette[index].b / 255.0f;

               x += 2;
               ++i;
            }
            else if (infoHeader.bitsPerPixel == 8) {
               r.data[px + 0] = palette[rawData[i]].r / 255.0f;
               r.data[px + 1] = palette[rawData[i]].g / 255.0f;
               r.data[px + 2] = palette[rawData[i]].b / 255.0f;

               ++x;
               ++i;
            }
         }
         else {
            if (r.channels == 3) {
               r.data[px + 0] = rawData[i + 2] / 255.0f;
               r.data[px + 1] = rawData[i + 1] / 255.0f;
               r.data[px + 2] = rawData[i + 0] / 255.0f;
            }
            else if (r.channels == 4) {
               r.data[px + 0] = rawData[i + 3] / 255.0f;
               r.data[px + 1] = rawData[i + 2] / 255.0f;
               r.data[px + 2] = rawData[i + 1] / 255.0f;
               r.data[px + 3] = rawData[i + 0] / 255.0f;
            }

            ++x;
            i += r.channels;
         }

         if (padSize > 0 && i % rowSize >= rowSize - 3) {
            if (isReversed) {
               --y;
            }
            else {
               ++y;
            }

            i += padSize;
         }
      }

      delete[] palette;
      delete[] rawData;

      return r;
   }


   Texture* Texture::currTexture = nullptr;
   Texture* Texture::prevTexture = nullptr;


   Texture::Texture() :
      _textureTarget(GL_TEXTURE_2D),
      _textureID(0),
      _image(),
      _filterMip(MF_NONE),
      _filterMin(TF_POINT),
      _filterMag(TF_POINT)
   {
      _wrap[0] = TW_REPEAT;
      _wrap[1] = TW_REPEAT;
      _wrap[2] = TW_REPEAT;
   }


   Texture::~Texture()
   {
      if (_textureID != 0) {
         glDeleteTextures(1, &_textureID);

         // res: Not sure about texture owning image data... hm.
         delete[] _image.data;
      }
   }


   Texture::operator bool() const
   {
      return _textureID != 0 && _image.data != nullptr;
   }


   void Texture::upload()
   {
   #ifdef DEBUG
      assert(_textureID != 0);
      GLenum errorGL = GL_NO_ERROR;
   #endif

      prevTexture = currTexture;
      currTexture = this;

      while (glGetError() != GL_NO_ERROR);
      glPushAttrib(GL_ENABLE_BIT);

      glEnable(_textureTarget);
      glBindTexture(_textureTarget, _textureID);

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::upload()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      bool hasMipmap = (_filterMip != MF_NONE);

   #ifndef GLUTEN_TEXTURE_MIPMAP_FALLBACK
      bool hasGenerateMipmap = 
         (GLEW_ARB_framebuffer_object || GLEW_EXT_framebuffer_object);

      if (hasMipmap && !hasGenerateMipmap) {
         glTexParameteri(_textureTarget, GL_GENERATE_MIPMAP, GL_TRUE);
      }
   #endif

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::upload()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      GLint texFormat = GL_RGB;
      GLenum gfxFormat = GL_RGB;

      if (_image.channels == 4) {
         texFormat = GL_RGBA;
         gfxFormat = GL_RGBA;
      }

      if (GLEW_EXT_texture_filter_anisotropic) {
         GLfloat maxAnisotropy = 0.0f;
         glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAnisotropy);
         glTexParameterf(_textureTarget, GL_TEXTURE_MAX_ANISOTROPY_EXT,
            maxAnisotropy);
      }

   #ifndef GLUTEN_TEXTURE_MIPMAP_FALLBACK
      if (_textureTarget == GL_TEXTURE_2D) {
         glTexImage2D(_textureTarget, 0, texFormat, _image.width, 
            _image.height, 0, gfxFormat, GL_FLOAT, _image.data);
      }
      else if (_textureTarget == GL_TEXTURE_3D) {
         glTexImage3D(_textureTarget, 0, texFormat, _image.width,
            _image.height, _image.layers, 0, gfxFormat, GL_FLOAT, 
            _image.data);
      }
   #else
      if (_textureTarget == GL_TEXTURE_2D) {
         if (!hasMipmap) {
            glTexImage2D(_textureTarget, 0, texFormat, _image.width, 
               _image.height, 0, gfxFormat, GL_FLOAT, _image.data);
         }
         else {
            gluBuild2DMipmaps(_textureTarget, texFormat, _image.width,
               _image.height, gfxFormat, GL_FLOAT, _image.data);
         }
      }
      else if (_textureTarget == GL_TEXTURE_3D) {
         if (!hasMipmap) {
            glTexImage3D(_textureTarget, 0, texFormat, _image.width,
               _image.height, _image.layers, 0, gfxFormat, GL_FLOAT, 
               _image.data);
         }
         else {
            g_reports("error", "Texture:upload()",
               "unsupported texture target for mipmapping (" +
               std::to_string(static_cast<long long>(_textureTarget)) + ")");
         }
      }
   #endif
      else {
         g_reports("error", "Texture::upload()", 
            "unsupported texture target (" + 
            std::to_string(static_cast<long long>(_textureTarget)) + ")");
      }

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::upload()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

   #ifndef GLUTEN_TEXTURE_MIPMAP_FALLBACK
      if (hasMipmap && hasGenerateMipmap) {
         if (GLEW_ARB_framebuffer_object) {
            glGenerateMipmap(_textureTarget);
         }
         else if (GLEW_EXT_framebuffer_object) {
            glGenerateMipmapEXT(_textureTarget);
         }
      }
   #endif

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::upload()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      currTexture = prevTexture;
      prevTexture = this;

      if (currTexture != nullptr) {
         glEnable(currTexture->_textureTarget);
         glBindTexture(currTexture->_textureTarget, currTexture->_textureID);
      }

      glPopAttrib();
   }


   GLenum toInternalWrap(TextureWrap w)
   {
      switch (w) {
         case TW_CLAMP_BORDER:   return GL_CLAMP_TO_BORDER;
         case TW_CLAMP_EDGE:     return GL_CLAMP_TO_EDGE;
         case TW_REPEAT:         return GL_REPEAT; 
         case TW_REPEAT_MIRROR:  return GL_MIRRORED_REPEAT;
         default:                return GL_REPEAT;
      }
   }


   GLenum toInternalTexFilter(TextureFilter f)
   {
      switch (f) {
         case TF_POINT:       return GL_NEAREST;
         case TF_BILINEAR:    return GL_LINEAR;
         default:             return GL_NEAREST;
      }
   }


   GLenum toInternalMinFilter(TextureFilter min, MipmapFilter mip)
   {
      if (mip == MF_NONE) {
         return toInternalTexFilter(min);
      }
      else {
         if (min == TF_POINT) {
            if (mip == MF_POINT) {
               return GL_NEAREST_MIPMAP_NEAREST;
            }
            else if (mip == MF_TRILINEAR) {
               return GL_NEAREST_MIPMAP_LINEAR;
            }
         }
         else if (min == TF_BILINEAR) {
            if (mip == MF_POINT) {
               return GL_LINEAR_MIPMAP_NEAREST;
            }
            else if (mip == MF_TRILINEAR) {
               return GL_LINEAR_MIPMAP_LINEAR;
            }
         }
      }

      return GL_NEAREST;
   }


   void Texture::commit()
   {
   #ifdef DEBUG
      assert(_textureID != 0);
      GLenum errorGL = GL_NO_ERROR;
   #endif

      prevTexture = currTexture;
      currTexture = this;

      while (glGetError() != GL_NO_ERROR);
      glPushAttrib(GL_ENABLE_BIT);

      glEnable(_textureTarget);
      glBindTexture(_textureTarget, _textureID);

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::commit()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      glTexParameteri(_textureTarget, GL_TEXTURE_WRAP_S, 
         toInternalWrap(_wrap[0]));
      glTexParameteri(_textureTarget, GL_TEXTURE_WRAP_T, 
         toInternalWrap(_wrap[1]));
      if (_textureTarget == GL_TEXTURE_3D) {
         glTexParameteri(_textureTarget, GL_TEXTURE_WRAP_R, 
            toInternalWrap(_wrap[2]));
      }

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::commit()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      glTexParameteri(_textureTarget, GL_TEXTURE_MIN_FILTER, 
         toInternalMinFilter(_filterMin, _filterMip));
      glTexParameteri(_textureTarget, GL_TEXTURE_MAG_FILTER,
         toInternalTexFilter(_filterMag));

      // TODO Mipmap parameters.

   #ifdef DEBUG
      while ((errorGL = glGetError()) != GL_NO_ERROR) {
         g_reports("debug,error", "Texture::commit()",
            "internal GL error (error " + 
            std::to_string(static_cast<long long>(errorGL)) + ")");
      }
   #endif

      currTexture = prevTexture;
      prevTexture = this;

      if (currTexture != nullptr) {
         glEnable(currTexture->_textureTarget);
         glBindTexture(currTexture->_textureTarget, currTexture->_textureID);
      }

      glPopAttrib();
   }


   void Texture::bind()
   {
      prevTexture = currTexture;
      currTexture = this;

      while (glGetError() != GL_NO_ERROR);
      glBindTexture(_textureTarget, _textureID);
   }


   void Texture::unbind()
   {
      currTexture = prevTexture;
      prevTexture = this;

      while (glGetError() != GL_NO_ERROR);

      if (currTexture != nullptr) {
         glPushAttrib(GL_ENABLE_BIT);
         glEnable(currTexture->_textureTarget);
         glBindTexture(currTexture->_textureTarget, currTexture->_textureID);
         glPopAttrib();
      }
      else {
         glBindTexture(_textureTarget, 0);
      }
   }


   void Texture::setImage(Image& image)
   {
      delete[] _image.data;
      
      _image = image;
      image.data = nullptr;
   }


   void Texture::setFilters(
      TextureFilter min, TextureFilter mag, MipmapFilter mip)
   {
      _filterMin = min;
      _filterMag = mag;
      _filterMip = mip;
   }


   void Texture::setWraps(TextureWrap s, TextureWrap t, TextureWrap r)
   {
      _wrap[0] = s;
      _wrap[1] = t;
      _wrap[2] = r;
   }


   TextureFactory::TextureFactory() :
      _filterMip(MF_NONE),
      _filterMin(TF_POINT),
      _filterMag(TF_POINT)
   {
      _wrap[0] = TW_REPEAT;
      _wrap[1] = TW_REPEAT;
      _wrap[2] = TW_REPEAT;
   }


   void TextureFactory::setDefaultFilters(
      TextureFilter min, TextureFilter mag, MipmapFilter mip)
   {
      _filterMin = min;
      _filterMag = mag;
      _filterMip = mip;
   }


   void TextureFactory::setDefaultWraps(
      TextureWrap s, TextureWrap t, TextureWrap r)
   {
      _wrap[0] = s;
      _wrap[1] = t;
      _wrap[2] = r;
   }
}

